/*******************************************************************************
 * Copyright (c) 2012-2015 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * <p/>
 * Contributors:
 * Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.jdt;

import com.google.inject.Inject;
import com.google.inject.Singleton;

import org.eclipse.che.dto.server.DtoFactory;
import org.eclipse.che.ide.ext.java.shared.dto.ImplementationsDescriptorDTO;
import org.eclipse.che.ide.ext.java.shared.dto.Region;
import org.eclipse.che.ide.ext.java.shared.dto.model.Member;
import org.eclipse.che.ide.ext.java.shared.dto.model.Type;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jdt.core.ICodeAssist;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.corext.util.MethodOverrideTester;

import java.util.ArrayList;
import java.util.List;

import static java.util.Collections.emptyList;

/**
 * A type hierarchy provides navigations between a type and its resolved supertypes
 * and subtypes for a specific type or for all types within a region.
 *
 * @author Valeriy Svydenko
 */
@Singleton
public class JavaTypeHierarchy {

    @Inject
    public JavaTypeHierarchy() {

    }

    /**
     * Get all implementations of selected Java Element.
     *
     * @param project
     *         opened project
     * @param fqn
     *         fully qualified name of the class file
     * @param offset
     *         cursor position
     * @return descriptor of the implementations
     * @throws JavaModelException
     *         when JavaModel has a failure
     */
    public ImplementationsDescriptorDTO getImplementations(IJavaProject project, String fqn, int offset) throws JavaModelException {
        ImplementationsDescriptorDTO implementationDescriptor = DtoFactory.newDto(ImplementationsDescriptorDTO.class);

        IJavaElement element = getJavaElement(project, fqn, offset);
        if (element == null){
            return implementationDescriptor.withImplementations(emptyList());
        }

        List<Type> implementations = new ArrayList<>();

        implementationDescriptor.setImplementations(implementations);

        switch (element.getElementType()) {
            case 7: //type
                findSubTypes(element, implementations);
                implementationDescriptor.setMemberName(element.getElementName());
                break;
            case 9: //method
                findTypesWithSubMethods(element, implementations);
                implementationDescriptor.setMemberName(element.getElementName());
                break;
            default:
                break;
        }

        return implementationDescriptor;
    }

    private IJavaElement getJavaElement(IJavaProject project, String fqn, int offset) throws JavaModelException {
        IJavaElement originalElement = null;
        IType type = project.findType(fqn);
        ICodeAssist codeAssist;
        if (type.isBinary()) {
            codeAssist = type.getClassFile();
        } else {
            codeAssist = type.getCompilationUnit();
        }

        IJavaElement[] elements = null;
        if (codeAssist != null) {
            elements = codeAssist.codeSelect(offset, 0);
        }

        if (elements != null && elements.length > 0) {
            originalElement = elements[0];
        }
        return originalElement;
    }

    private void findSubTypes(IJavaElement element, List<Type> implementations) throws JavaModelException {
        IType type = (IType)element;
        ITypeHierarchy typeHierarchy = type.newTypeHierarchy(new NullProgressMonitor());
        IType[] implTypes = typeHierarchy.getAllSubtypes(type);

        for (IType implType : implTypes) {
            Type dto = convertToTypeDTO(implType);
            implementations.add(dto);
        }
    }

    private void findTypesWithSubMethods(IJavaElement element, List<Type> implementations) throws JavaModelException {
        IMethod selectedMethod = (IMethod)element;
        IType parentType = selectedMethod.getDeclaringType();
        if (parentType == null) {
            return;
        }
        ITypeHierarchy typeHierarchy = parentType.newTypeHierarchy(new NullProgressMonitor());
        IType[] subTypes = typeHierarchy.getAllSubtypes(parentType);

        MethodOverrideTester methodOverrideTester = new MethodOverrideTester(parentType, typeHierarchy);

        for (IType type : subTypes) {
            IMethod method = methodOverrideTester.findOverridingMethodInType(type, selectedMethod);
            if (method == null) {
                continue;
            }
            Type openDeclaration = convertToTypeDTO(type);
            setRange(openDeclaration, method);
            implementations.add(openDeclaration);
        }
    }

    private void setRange(Member member, IMember iMember) throws JavaModelException {
        ISourceRange nameRange = iMember.getNameRange();
        if (iMember.isBinary()) {
            nameRange = iMember.getSourceRange();
        }

        if (nameRange == null) {
            return;
        }

        member.setFileRegion(convertToRegionDTO(iMember.getSourceRange()));
    }

    private Type convertToTypeDTO(IType type) throws JavaModelException {
        Type dto = DtoFactory.newDto(Type.class);
        String typeName = type.getElementName();
        if (typeName.isEmpty()) {
            dto.setElementName("Anonymous in " + type.getParent().getElementName());
        } else {
            dto.setElementName(type.getElementName());
        }
        if (type.isBinary()) {
            dto.setRootPath(type.getFullyQualifiedName());
            dto.setLibId(type.getAncestor(IPackageFragmentRoot.PACKAGE_FRAGMENT_ROOT).hashCode());
            dto.setBinary(true);
        } else {
            dto.setRootPath(type.getPath().toOSString());
            dto.setBinary(false);
        }
        setRange(dto, type);

        return dto;
    }

    private Region convertToRegionDTO(ISourceRange iSourceRange) {
        Region region = DtoFactory.newDto(Region.class);
        return iSourceRange == null ? region : region.withLength(iSourceRange.getLength()).withOffset(iSourceRange.getOffset());
    }
}
